import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import TWEEN from 'three/examples/jsm/libs/tween.module.js';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
import { merge, debounce } from 'lodash-es';
import ResourceTracker from './ResourceTracker';
import { log } from './utils';

window.THREE = THREE; // 全局挂载THREE
window.TWEEN = TWEEN; // 全局挂载TWEEN

const EVENTS = ['mousemove', 'click']; // 鼠标事件

export default class BaseThree {
  constructor(elem, options) {
    if (typeof elem === 'string') {
      this.container = document.querySelector(elem);
    } else if (elem instanceof HTMLElement) {
      this.container = elem;
    }

    if (!this.container) {
      console.error('未找到挂载元素:', elem);
      return;
    }

    // 防止出现滚动条导致留白
    this.container.style.overflow = 'hidden';

    const defaultOptions = {
      debug: false,
      backgroundColor: 0x000000,
      interactive: false,
      css2DRenderer: false,
      css3DRenderer: false,
      camera: {
        fov: 45,
        near: 0.1,
        far: 1000,
        position: [0, 10, 15],
      },
      controls: {
        visible: true,
        enableDamping: true,
      },
      stats: {
        visible: true,
      },
      axes: {
        visible: true,
        size: 1,
      },
    };

    this.options = merge(defaultOptions, options, {
      width: this.container.offsetWidth,
      height: this.container.offsetHeight,
      rect: this.container.getBoundingClientRect(),
    });

    this.scene = new THREE.Scene();
    this.camera = null;
    this.renderer = null;
    this.controls = null;
    this.stats = null;
    this.raycaster = null;
    this.clock = null;
    this.tracker = new ResourceTracker();
    this.mouse = null;
    this.disposed = false;

    this.#init();
  }

  #modelList = [];

  #init() {
    this.#initStats();
    this.#initCamera();
    this.#initRenderer();
    this.#initAxes();
    this.#initControls();
    this.#initRaycaster();

    this.clock = new THREE.Clock();
    this.renderer.setAnimationLoop(this.#animate);
    window.addEventListener('resize', this.#onResize);

    if (this.options.debug) {
      log(this.renderer.info);
    }
  }

  addModel(model) {
    if (model) {
      this.#modelList.push(model);
    }
  }

  #initStats() {
    const { debug, stats } = this.options;

    if (!debug || !stats.visible) return;

    this.stats = new Stats();
    this.container.appendChild(this.stats.dom);
  }

  #initCamera() {
    const {
      width,
      height,
      camera: { fov, near, far, position },
    } = this.options;
    const aspect = width / height;

    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
    camera.position.set(...position);
    camera.lookAt(0, 0, 0);
    this.camera = camera;
  }

  #initRenderer() {
    const { width, height, backgroundColor, css2DRenderer, css3DRenderer } = this.options;
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    renderer.setClearColor(backgroundColor, 1);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.BasicShadowMap;
    this.container.appendChild(renderer.domElement);
    this.renderer = renderer;

    // css2D渲染器
    if (css2DRenderer) {
      const css2DRenderer = new CSS2DRenderer();
      css2DRenderer.setSize(width, height);
      css2DRenderer.domElement.style.position = 'absolute';
      css2DRenderer.domElement.style.top = '0px';
      css2DRenderer.domElement.style.left = '0px';
      this.container.appendChild(css2DRenderer.domElement);
      this.css2DRenderer = css2DRenderer;
    }

    // css3D渲染器
    if (css3DRenderer) {
      const css3DRenderer = new CSS3DRenderer();
      css3DRenderer.setSize(width, height);
      css3DRenderer.domElement.style.position = 'absolute';
      css3DRenderer.domElement.style.top = '0px';
      css3DRenderer.domElement.style.left = '0px';
      this.container.appendChild(css3DRenderer.domElement);
      this.css3DRenderer = css3DRenderer;
    }
  }

  #initAxes() {
    const {
      debug,
      axes: { visible, size },
    } = this.options;

    if (!debug || !visible) return;

    const axesHelper = new THREE.AxesHelper(size);
    this.scene.add(axesHelper);
  }

  #initControls() {
    let {
      controls: { visible, ...attrs },
    } = this.options;
    if (!visible) return false;

    try {
      const renderer = this.css3DRenderer ?? this.css2DRenderer ?? this.renderer;
      const controls = new OrbitControls(this.camera, renderer.domElement);

      Object.entries(attrs).forEach(([k, v]) => {
        if (Array.isArray(v)) {
          controls[k] = new THREE.Vector3(...v);
        } else {
          controls[k] = v;
        }
      });

      this.controls = controls;
    } catch (err) {
      console.error(err);
    }
  }

  #initRaycaster() {
    if (!this.options.interactive) return;

    const raycaster = new THREE.Raycaster();
    this.raycaster = raycaster;

    EVENTS.forEach((event) => {
      this.container.addEventListener(event, this.#onMouseEvent);
    });
  }

  #onMouseEvent = (e) => {
    e.preventDefault();
    const { width, height, rect } = this.options;
    this.mouse ??= { x: 0, y: 0 };
    this.mouse.x = ((e.clientX - rect.left) / width) * 2 - 1;
    this.mouse.y = -((e.clientY - rect.top) / height) * 2 + 1;
    this.#updateRaycaster(e.type);
  };

  #updateRaycaster(type) {
    if (!this.mouse) return;

    this.raycaster.setFromCamera(this.mouse, this.camera);

    this.#modelList.forEach((model) => {
      if (model.listener?.[type]) {
        model.listener[type].forEach(([obj, handler]) => {
          const intersects = this.raycaster.intersectObject(obj);
          handler(intersects);
        });
      }
    });
  }

  #onResize = debounce(() => {
    const width = this.container.offsetWidth;
    const height = this.container.offsetHeight;
    const rect = this.container.getBoundingClientRect();
    const aspect = width / height;

    this.options.width = width;
    this.options.height = height;
    this.options.rect = rect;

    this.camera.aspect = aspect;
    this.camera.updateProjectionMatrix();

    this.renderer.setSize(width, height);
    this.css2DRenderer?.setSize(width, height);
    this.css3DRenderer?.setSize(width, height);
  }, 100);

  #animate = (time) => {
    const { stats, controls } = this.options;
    const delta = this.clock.getDelta();

    this.stats?.update();

    this.controls?.update();

    this.#modelList.forEach((model) => {
      if (typeof model.tick === 'function') {
        model.tick(time, delta);
      }
    });

    TWEEN.update(time);
    this.renderer.render(this.scene, this.camera);
    this.css2DRenderer?.render(this.scene, this.camera);
    this.css3DRenderer?.render(this.scene, this.camera);
  };

  dispose() {
    if (this.disposed) return;

    window.removeEventListener('resize', this.#onResize);

    EVENTS.forEach((event) => {
      this.container.removeEventListener(event, this.#onMouseEvent);
    });
    this.mouse = null;

    this.#modelList.forEach((model) => {
      if (typeof model.dispose === 'function') {
        model.dispose();
      }
    });
    this.#modelList = [];

    if (this.tracker) {
      this.tracker.dispose(this.scene);
      this.tracker.disposeAll();
      this.tracker = null;
    }

    if (this.stats) {
      this.container.removeChild(this.stats.dom);
      this.stats = null;
    }

    if (this.scene) {
      this.scene.clear();
    }

    if (this.camera) {
      this.camera.clear();
    }

    if (this.options.debug) {
      log(this.renderer.info);
    }

    if (this.renderer) {
      this.renderer.setAnimationLoop(null);
      this.renderer.dispose();
      this.renderer.forceContextLoss();
      this.renderer.content = null;
      let gl = this.renderer.domElement.getContext('webgl');
      gl && gl.getExtension('WEBGL_lose_context').loseContext();
      this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
    }

    if (this.css2DRenderer) {
      this.container.removeChild(this.css2DRenderer.domElement);
      this.css2DRenderer = null;
    }

    if (this.css3DRenderer) {
      this.container.removeChild(this.css3DRenderer.domElement);
      this.css3DRenderer = null;
    }

    this.scene = null;
    this.camera = null;
    this.renderer = null;
    this.controls = null;
    this.stats = null;
    this.raycaster = null;
    this.clock = null;

    this.disposed = true;
  }
}
